package com.rizzo.web.util;

import javax.faces.model.SelectItem;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * Enum utility class for generating and caching lists of {@link javax.faces.model.SelectItem}s.
 * These {@link javax.faces.model.SelectItem}s are used in selection components in JSF. By using
 * the facelet's taglib function library, we can use this class inside facelet
 * pages.
 * <p/>
 * Basically, this class defines static convenience methods to get a
 * {@link javax.faces.model.SelectItem} list for a certain {@link Enum} type and one private
 * method which transforms the enum constant of a given {@link Enum} into a
 * {@link javax.faces.model.SelectItem} list through java reflection.
 * <p/>
 * Should contain following method declarations;
 * public static List<SelectItem> refreshRates() {
 * return generic(RefreshRateEnum.class, "description", "refreshRate", null);
 * }
 *
 * @author Jan Lievens
 */
public class EnumUtil {

    private final static HashMap<String, List<SelectItem>> CACHE = new HashMap<String, List<SelectItem>>();

    /**
     * Constructor EnumUtil creates a new EnumUtil instance.
     */
    private EnumUtil() {
    }

    /**
     * Returns a {@link SelectItem} list representation for the
     * {@link LocaleEnum} enum, using the
     * {@link LocaleEnum#getFullQualifiedLanguageName()} as label.
     *
     * @return
     */
    public static List<SelectItem> locales() {
        return generic(LocaleEnum.class, "fullQualifiedLanguageName", "locales", null);
    }

    /**
     * Generates a {@link javax.faces.model.SelectItem} list for the given {@link Enum} class
     * through reflection.
     * <p/>
     * The method first checks if the {@link javax.faces.model.SelectItem} list is already
     * present.
     * <p/>
     * If the list is not in the cache, it will construct it, by checking if the
     * class is actually an enum, then extracting all it's enum constants (see
     * {@link Class#getEnumConstants()}), sorting them by the given comparator
     * (if any) and iterating over them. For each iteration a {@link javax.faces.model.SelectItem}
     * is generated containing the {@link Enum#name()} as key, and a label
     * derived from the getter method for the labelProperty
     * <p/>
     * After iterating over all the {@link Enum}'s items, the generated list is
     * stored in the CACHE constant. Notice that the cached lists are never
     * filtered. Exludes aren't considered while generating cached lists.
     * <p/>
     * If excludes are given, a new list is generated based on the cached
     * version and the {@link javax.faces.model.SelectItem} having the same key as the exclude
     * value are removed from this new list. Filtered list will not be cached.
     *
     * @param enumClass     The enum type.
     * @param labelProperty the property of the enum type to be used as label.
     * @param cacheName     the key of the {@link javax.faces.model.SelectItem} list in cache.
     * @param comparator    comparator to sort the list of enums, if needed. If the
     *                      standard ordering (ordinal) is ok, set this to null.
     * @param exclude       A list of string values representing the {@link Enum#name()}
     *                      values which should be excluded from the list of
     *                      {@link javax.faces.model.SelectItem}s.
     * @return A List of prepared {@link javax.faces.model.SelectItem}.
     * @throws IllegalArgumentException when the paramClass is not an enum and when the labelProperty
     *                                  has no valid getter method.
     */
    private static <T extends Enum<?>> List<SelectItem> generic(Class<T> enumClass, String labelProperty,
                                                                String cacheName, Comparator<T> comparator, String... exclude) {
        List<SelectItem> cached = CACHE.get(cacheName);
        if (cached == null) {
            cached = new ArrayList<SelectItem>();
            if (!enumClass.isEnum()) {
                throw new IllegalArgumentException(enumClass.getName() + " is not an enum class!");
            }

            Method getMethod;
            String getter = "get" + Character.toUpperCase(labelProperty.charAt(0)) + labelProperty.substring(1);
            try {
                getMethod = enumClass.getMethod(getter);

                T[] enumValues = enumClass.getEnumConstants();
                if (comparator != null) {
                    Arrays.sort(enumValues, comparator);
                }

                for (Enum<?> enumValue : enumValues) {
                    // Don't consider excludes, we want to cache the whole list!
                    // Filtering happens afterward
                    cached.add(new SelectItem(enumValue.name(), getMethod.invoke(enumValue).toString()));
                }
                CACHE.put(cacheName, cached);
            } catch (SecurityException e) {
                throw new IllegalArgumentException(getter + " is probably not public!", e);
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException(getter + " is not a method of " + enumClass.getName(), e);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(getter + " is not a standard getter method.", e);
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException(getter + " is not a method of " + enumClass.getName(), e);
            } catch (InvocationTargetException e) {
                throw new IllegalArgumentException(getter + " throw an exception, see root cause", e);
            }

        }

        if (exclude != null && exclude.length > 0) {
            ArrayList<SelectItem> filtered = new ArrayList<SelectItem>(cached);

            for (int i = filtered.size() - 1; i > -1; i--) {
                // Iterate the filtered collection reversely, because we need to
                // remove some items.
                for (String excluded : exclude) {
                    if (excluded.equals(filtered.get(i).getValue())) {
                        filtered.remove(i);
                        break;
                    }
                }
            }

            return filtered;
        }

        return cached;
	}
}